package org.easyupdater;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Set;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;
import org.easyupdater.model.ApplicationSpec;
import org.easyupdater.model.BundleSpec;
import org.osgi.framework.Version;

/**
 * @author Bogdan Dimitriu
 */
public class ApplicationManager implements Runnable {

	private static final Logger log = Logger.getLogger(ApplicationManager.class);

	private volatile File currentApplicationSpecFile;

	private String applicationUpdateSiteURL;

	private File bundlesDeployDirectory;

	private IBundleUpgradeManager bundleUpgradeManager;

	private IBackupManager backupManager;

	private IOsgiBundleHandler osgiBundleHandler;

	private IDownloadManager downloadManager;

	private IApplicationSpecSerializer applicationSpecSerializer;

	/**
	 * The URL of the update site XML file
	 */
	private URL updateSiteSpecFileURL;

	public void run() {
		try {
			log.info("Checking for updates...");
			runUpdate();
			log.info("Finished update process");
		} catch (Exception e) {
			log.error("Error while updating", e);
		}
	}

	private void runUpdate() {
		ApplicationSpec currentApplicationSpec = loadCurrentApplicationSpec();
		if (currentApplicationSpec == null) {
			log.info("No current application specification found");
			return;
		}

		ApplicationSpec newApplicationSpec = loadNewApplicationSpec();
		if (newApplicationSpec == null) {
			log.warn("Could not download new application spec from the update URL");
			return;
		}

		Set<BundleSpec> bundlesToRemove = null;
		Set<BundleSpec> bundlesToInstall = null;
		boolean hasApplicationSpecVersionChanged = checkApplicationVersionHasChanged(
				currentApplicationSpec, newApplicationSpec);
		if (!hasApplicationSpecVersionChanged) {
			log.info("The application spec version hasn't changed - no update will be done");
			return;
		}
		bundlesToRemove = bundleUpgradeManager.getBundlesToRemove(currentApplicationSpec,
				newApplicationSpec);
		bundlesToInstall = bundleUpgradeManager.getBundlesToInstall(currentApplicationSpec,
				newApplicationSpec);
		if (bundlesToInstall.size() == 0 && bundlesToRemove.size() == 0) {
			log.info("No bundles have changed, just the application spec version");
		}

		boolean downloadSuccess = downloadNewBundles(bundlesToInstall);
		if (!downloadSuccess)
			return;

		boolean backupSuccess = backupManager.backupApplication(currentApplicationSpec);
		if (!backupSuccess) {
			log.error("Could not perform backup of the application. "
					+ "The update will be aborted");
			return;
		}

		removeOldBundles(bundlesToRemove);
		boolean successfulInstallation = installNewBundles(bundlesToInstall);

		if (!successfulInstallation) {
			// TODO do rollback
		} else {
			currentApplicationSpec = newApplicationSpec;
			saveNewApplicationSpec(currentApplicationSpec);
		}
	}

	private boolean checkApplicationVersionHasChanged(ApplicationSpec currentApplicationSpec,
			ApplicationSpec newApplicationSpec) {
		Version oldVersion = new Version(currentApplicationSpec.getVersion());
		Version newVersion = new Version(newApplicationSpec.getVersion());
		return !oldVersion.equals(newVersion);
	}

	private void saveNewApplicationSpec(ApplicationSpec currentApplicationSpec) {
		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(currentApplicationSpecFile);
			// note that the file saved on the disk might not have the
			// elements in the same order as the original remote file!
			applicationSpecSerializer.saveApplicationSpec(currentApplicationSpec, fos);
		} catch (Exception e) {
			log.error("Could not save application spec file", e);
		} finally {
			IOUtils.closeQuietly(fos);
		}
	}

	private boolean installNewBundles(Set<BundleSpec> bundlesToInstall) {
		// install the new bundles
		boolean successfulInstallation = true;
		if (bundlesToInstall.size() > 0) {
			for (BundleSpec bundleSpec : bundlesToInstall) {
				log.info("Installing bundle: " + bundleSpec);
				try {
					File newFile = new File(bundlesDeployDirectory, bundleSpec.getLocalJar()
							.getName());
					if (newFile.exists()) {
						log.warn("Destination file already exists: "
								+ bundleSpec.getLocalJar().getName() + ". Overwriting it.");
						boolean deleted = newFile.delete();
						if (!deleted)
							log.error("Could not delete file: "
									+ bundleSpec.getLocalJar().getName());
					}
					FileUtils.moveFile(bundleSpec.getLocalJar(), newFile);
					bundleSpec.setLocalJar(newFile);
					boolean installOk = osgiBundleHandler.installBundle(bundleSpec);
					if (!installOk) {
						successfulInstallation = false;
						break;
					}
				} catch (Exception e) {
					log.error("", e);
					successfulInstallation = false;
					break;
				}
			}
			// start new bundles
			osgiBundleHandler.startBundles(bundlesToInstall);
		}
		return successfulInstallation;
	}

	private void removeOldBundles(Set<BundleSpec> bundlesToRemove) {
		// remove bundles
		if (bundlesToRemove.size() > 0) {
			osgiBundleHandler.stopBundles(bundlesToRemove);
			for (BundleSpec bundleSpec : bundlesToRemove) {
				osgiBundleHandler.removeBundle(bundleSpec);
			}
		}
	}

	/**
	 * Downloads the new bundles that have to be installed
	 * 
	 * @param bundlesToInstall
	 *            the set of bundles that need to be downloaded
	 * @return boolean value indicating whether all the bundles were downloaded
	 *         successfully or not. If no bundles are to be downloaded then this
	 *         method will also return <code>true</code>
	 */
	private boolean downloadNewBundles(Set<BundleSpec> bundlesToInstall) {
		// download the new bundles
		if (bundlesToInstall.size() > 0) {
			for (BundleSpec bundleSpec : bundlesToInstall) {
				boolean downloadSuccessful = downloadManager.downloadBundle(
						applicationUpdateSiteURL, bundleSpec);
				if (downloadSuccessful) {
					log.info("Bundle " + bundleSpec + " has been downloaded successfully");
				} else {
					log.error("Bundle " + bundleSpec.getId()
							+ " could not be downloaded. Aborting update process.");
					return false;
				}
			}
		}
		return true;
	}

	private ApplicationSpec loadNewApplicationSpec() {
		ApplicationSpec newApplicationSpec = null;
		try {
			URLConnection updateSiteConnection = updateSiteSpecFileURL.openConnection();
			InputStream updateSiteInpuStream = updateSiteConnection.getInputStream();
			newApplicationSpec = applicationSpecSerializer
					.downloadApplicationSpec(updateSiteInpuStream);
			newApplicationSpec.setUrl(applicationUpdateSiteURL);
			IOUtils.closeQuietly(updateSiteInpuStream);
		} catch (Exception e) {
			log.warn("Could not download application specification from "
					+ applicationUpdateSiteURL + ". Application update is not available", e);
		}
		return newApplicationSpec;
	}

	/**
	 * Load and validate the current application specification
	 * 
	 * @return the application specification instance in case the application
	 *         spec has been loaded successfully or <code>null</code> in case it
	 *         has not been loaded or validated successfully
	 */
	private ApplicationSpec loadCurrentApplicationSpec() {
		ApplicationSpec currentApplicationSpec = null;
		if (currentApplicationSpecFile != null && currentApplicationSpecFile.exists()) {
			try {
				currentApplicationSpec = applicationSpecSerializer
						.downloadApplicationSpec(new BufferedInputStream(new FileInputStream(
								currentApplicationSpecFile)));
				if (!validateCurrentApplicationSpec(currentApplicationSpec))
					currentApplicationSpec = null;
			} catch (Exception e) {
				log.error("Could not read local application spec file", e);
			}
		}
		return currentApplicationSpec;
	}

	/**
	 * Checks if the bundles defined in the local application specification file
	 * actually exist
	 * 
	 * @param currentApplicationSpec
	 *            the local application specification
	 * @return boolean value indicating whether the validation was successful
	 */
	boolean validateCurrentApplicationSpec(ApplicationSpec currentApplicationSpec) {
		for (BundleSpec bundleSpec : currentApplicationSpec.getBundleSpecs()) {
			String bundleId = bundleSpec.getId();
			String bundleVersionString = bundleSpec.getVersion();
			String jarName = bundleId + "-" + bundleVersionString + ".jar";
			File bundleJar = new File(bundlesDeployDirectory, jarName);
			if (!bundleJar.exists() || !bundleJar.isFile()) {
				log.warn("The local application specification refers a bundle that doesn't exist: "
						+ jarName);
				return false;
			} else {
				bundleSpec.setLocalJar(bundleJar);
			}
		}
		return true;
	}

	public String getApplicationUpdateSiteURL() {
		return applicationUpdateSiteURL;
	}

	public void setApplicationUpdateSiteURL(String applicationUpdateSiteURL)
			throws MalformedURLException {
		this.applicationUpdateSiteURL = applicationUpdateSiteURL;
		String updateSiteSpecFileURLString = applicationUpdateSiteURL
				.charAt(applicationUpdateSiteURL.length() - 1) == '/' ? applicationUpdateSiteURL
				: applicationUpdateSiteURL + "/";
		updateSiteSpecFileURLString += Constants.UPDATE_SITE_SPEC_FILE_NAME;
		updateSiteSpecFileURL = new URL(updateSiteSpecFileURLString);
	}

	public File getCurrentApplicationSpec() {
		return currentApplicationSpecFile;
	}

	public void setCurrentApplicationSpec(File currentApplicationSpecFile) {
		this.currentApplicationSpecFile = currentApplicationSpecFile;
	}

	public IBundleUpgradeManager getBundleUpgradeManager() {
		return bundleUpgradeManager;
	}

	public void setBundleUpgradeManager(IBundleUpgradeManager bundleUpgradeManager) {
		this.bundleUpgradeManager = bundleUpgradeManager;
	}

	public IBackupManager getBackupManager() {
		return backupManager;
	}

	public void setBackupManager(IBackupManager backupManager) {
		this.backupManager = backupManager;
	}

	public IOsgiBundleHandler getOsgiBundleHandler() {
		return osgiBundleHandler;
	}

	public void setOsgiBundleHandler(IOsgiBundleHandler osgiBundleHandler) {
		this.osgiBundleHandler = osgiBundleHandler;
	}

	public File getBundlesDeployDirectory() {
		return bundlesDeployDirectory;
	}

	public void setBundlesDeployDirectory(File bundlesDeployDirectory) {
		this.bundlesDeployDirectory = bundlesDeployDirectory;
	}

	public IDownloadManager getDownloadManager() {
		return downloadManager;
	}

	public void setDownloadManager(IDownloadManager downloadManager) {
		this.downloadManager = downloadManager;
	}

	public IApplicationSpecSerializer getApplicationSpecSerializer() {
		return applicationSpecSerializer;
	}

	public void setApplicationSpecSerializer(IApplicationSpecSerializer applicationSpecSerializer) {
		this.applicationSpecSerializer = applicationSpecSerializer;
	}
}
